package phaseII;

import java.util.ArrayList;

@SuppressWarnings("unchecked")
public class Parser {

	Scanner scanner;
	VarTable varTable;
	FunctionCaller funcCaller;
	StringBuilder executionOutput;
	
	public Parser(String miniREFilePath) {
		scanner = new Scanner(miniREFilePath);
		varTable = new VarTable();
		funcCaller = new FunctionCaller(varTable);
		executionOutput = new StringBuilder();
		
		miniREProgram();
	}
	
	private void miniREProgram() {
		String token = scanner.peekToken();
		if (!matchToken(token, "begin"))
			syntaxError();
		statement_list();
		token = scanner.peekToken();
		if (!matchToken(token, "end"))
			syntaxError();
	}
	
	private void statement_list() {
		if (statement())
			statement_list_tail();
	}

	private void statement_list_tail() {
		if (statement())
			statement_list_tail();
		else
			return;//syntaxError();
	}

	private boolean statement() {
		if (scanner.peekTokenType() == TokenType.ID) {
			String varName = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.ID);
			matchToken(scanner.peekToken(), "=");
			
			if (scanner.peekTokenType() == TokenType.NUMBER_SYMBOL) {
				matchToken(scanner.peekToken(), "#");
				//Check if exp is an ID or a function call
				VarValue result = exp();
				
				if (result.getData() instanceof String) { //ID was returned
					String varID = (String) result.getData();
					Integer numMatches = funcCaller.matchedListLength(varID);
					if(numMatches == null)
						runtimeError(varID);
					assign(varName, new VarValue(numMatches, Integer.class));
				}
				else { //ArrayList of matched String is returned
					ArrayList<String> matches = (ArrayList<String>) result.getData();
					Integer size = matches.size();
					assign(varName, new VarValue(size, Integer.class));
				}
				
				matchTokenType(scanner.peekTokenType(), TokenType.SEMI_COLON);
				return true;
			}
			else if (scanner.peekToken().equals("maxfreqstring")) {
				matchToken(scanner.peekToken(), "maxfreqstring");
				matchTokenType(scanner.peekTokenType(), TokenType.OPEN_PARENT);
				
				if (scanner.peekTokenType() != TokenType.ID)
					syntaxError();
				
				String varID = scanner.peekToken();
				matchTokenType(scanner.peekTokenType(), TokenType.ID);
				String freqString = funcCaller.getMaxFreqString(varID);
				if (freqString == null)
					runtimeError(varID);
				assign(varName, new VarValue(freqString, String.class));
				
				matchTokenType(scanner.peekTokenType(), TokenType.CLOSE_PARENT);
				matchTokenType(scanner.peekTokenType(), TokenType.SEMI_COLON);
				return true;
			}
			else {
				VarValue result = exp();
				if (result.getData() instanceof String) { //ID was returned
					String varID = (String) result.getData();
					VarValue varVal = varTable.getVar(varID);
					if (varVal == null)
						runtimeError(varID);
					assign(varName, varVal);
				}
				else { //ArrayList of matched String is returned
					ArrayList<String> matches = (ArrayList<String>) result.getData();
					assign(varName, new VarValue(matches, ArrayList.class));
				}
				matchTokenType(scanner.peekTokenType(), TokenType.SEMI_COLON);
				return true;
			}
		}//End if ID
		else if (scanner.peekTokenType() == TokenType.FUNCTION) {
			function_call();
			return true;
		}
		else {
			//syntaxError();
			return false;
		}
	}
	
	private void function_call() {
		String function = scanner.peekToken();
		if (function.equals("print")) {
			matchToken(function, "print");
			if (scanner.peekTokenType() != TokenType.OPEN_PARENT)
				syntaxError();
			matchToken(scanner.peekToken(), "(");
			
			ArrayList<VarValue> varValues = exp_list();
			StringBuilder output = new StringBuilder();
			for (int i = 0; i < varValues.size(); i++) {
				VarValue varVal = varValues.get(i);
				String strToPrint = funcCaller.print(varVal);
				if (strToPrint == null)
					runtimeError(varVal.getData().toString());
				else {
					if (i == varValues.size()-1)
						output.append(strToPrint);
					else
						output.append(strToPrint+", ");
				}
			}
			executionOutput.append(output.toString()+"\n");
			
			
			matchToken(scanner.peekToken(), ")");
			matchToken(scanner.peekToken(), ";");
		}
		else if (function.equals("replace") || function.equals("recursivereplace")) {
			matchTokenType(scanner.peekTokenType(), TokenType.FUNCTION);
			if (scanner.peekTokenType() != TokenType.REGEX)
				syntaxError();
			String regex = scanner.peekToken();
			if (RegexValidator.validate(regex) == false)
				regexError(regex);
			matchTokenType(scanner.peekTokenType(), TokenType.REGEX);
			
			if (scanner.peekToken().equals("with") == false)
				syntaxError();
			matchToken(scanner.peekToken(), "with");
			
			if (scanner.peekTokenType() != TokenType.STRING)
				syntaxError();
			String replacement = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.STRING);
			
			if (scanner.peekToken().equals("in") == false)
				syntaxError();
			matchToken(scanner.peekToken(), "in");
			
			if (scanner.peekTokenType() != TokenType.STRING)
				syntaxError();
			String srcFile = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.STRING);
			
			if (scanner.peekTokenType() != TokenType.STORE)
				syntaxError();
			matchTokenType(scanner.peekTokenType(), TokenType.STORE);
			
			if (scanner.peekTokenType() != TokenType.STRING)
				syntaxError();
			String dstFile = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.STRING);
			
			if (function.equals("replace"))
				funcCaller.replace(regex, replacement, srcFile, dstFile);
			else
				funcCaller.recursiveReplace(regex, replacement, srcFile, dstFile);
			
			matchToken(scanner.peekToken(), ";");
		}
		else
			syntaxError();
	}
	
	private ArrayList<VarValue> exp_list() {
		ArrayList<VarValue> varValues = new ArrayList<VarValue>();
		VarValue exp = exp();
		if (exp != null) {
			varValues.add(exp);
			ArrayList<VarValue> exp_list_tail = exp_list_tail();
			if (exp_list_tail != null) {
				for (VarValue v : exp_list_tail)
					varValues.add(v);
			}
		}
		return varValues;
	}

	

	private ArrayList<VarValue> exp_list_tail() {
		if (scanner.peekToken().equals(",")) {
			matchToken(scanner.peekToken(), ",");
			ArrayList<VarValue> varValues = new ArrayList<VarValue>();
			VarValue exp = exp();
			if (exp != null) {
				varValues.add(exp);
				ArrayList<VarValue> exp_list_tail = exp_list_tail();
				if (exp_list_tail != null) {
					for (VarValue v : exp_list_tail)
						varValues.add(v);
				}
			}
			return varValues;
		}
		else
			return null;
	}

	private VarValue exp() {
		if (scanner.peekTokenType() == TokenType.ID) {
			String varID = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.ID);
			return new VarValue(varID, String.class);
		}
		else if (scanner.peekTokenType() == TokenType.OPEN_PARENT) {
			matchToken(scanner.peekToken(), "(");
			VarValue varVal = exp();
			if (scanner.peekTokenType() == TokenType.CLOSE_PARENT)
				matchToken(scanner.peekToken(), ")");
			return varVal;
		}
		else {
			VarValue term = term();
			if (term != null) {
				VarValue exp_tail = exp_tail(term);
				if (exp_tail != null)
					term = exp_tail;
			}
			return term;
		}
	}

	private VarValue exp_tail(VarValue leftTerm) {
		if (scanner.peekTokenType() == TokenType.SPECIAL) {
			String binOp = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.SPECIAL);
			
			VarValue term = term();
			
			//check if exp_tail() is used
			if (term != null) {
				VarValue exp_tail = exp_tail(term);
				if (exp_tail != null)
					term = exp_tail;
			}
			
			ArrayList<String> result = null;
			if (term != null && term.getData() instanceof ArrayList<?> && leftTerm.getData() instanceof ArrayList<?>) {
				ArrayList<String> leftData = (ArrayList<String>)leftTerm.getData();
				ArrayList<String> rightData = (ArrayList<String>)term.getData();
				if (binOp.equals("diff"))
					result = funcCaller.difference(leftData, rightData);
				else if (binOp.equals("union"))
					result = funcCaller.union(leftData, rightData);
				else if (binOp.equals("inters"))
					result = funcCaller.intersection(leftData, rightData);
			}
			
			if (result != null)
				return new VarValue(result, ArrayList.class);
			else
				return null;
		}
		else
			return null;
	}

	private VarValue term() {
		if (scanner.peekToken().equals("find")) {
			matchToken(scanner.peekToken(), "find");
			
			if (scanner.peekTokenType() != TokenType.REGEX)
				syntaxError();
			String regex = scanner.peekToken();
			if (RegexValidator.validate(regex) == false)
				regexError(regex);
				
			matchTokenType(scanner.peekTokenType(), TokenType.REGEX);
			
			if (!scanner.peekToken().equals("in"))
				syntaxError();
			matchToken(scanner.peekToken(), "in");
			
			if (scanner.peekTokenType() != TokenType.STRING)
				syntaxError();
			String fileName = scanner.peekToken();
			matchTokenType(scanner.peekTokenType(), TokenType.STRING);
			
			VarValue matchedString = funcCaller.findMatches(regex, fileName);
			return matchedString;
		}
		else {
			syntaxError();
			return null;
		}
	}
	
	private boolean matchToken(String tokenReceived, String tokenExpected) {
		if (tokenExpected.equals(tokenReceived)) {
			//Remove current token after it's matched
			System.out.println("Matched "+scanner.peekTokenType()+": "+scanner.getToken());
			return true;
		}
		else {
			syntaxError();
			return false;
		}
	}
	
	private boolean matchTokenType(TokenType receivedType, TokenType expectedType) {
		if (receivedType == expectedType) {
			System.out.println("Matched "+scanner.peekTokenType()+": "+scanner.getToken());
			return true;
		}
		else {
			syntaxError();
			return false;
		}
	}
	
	private void assign(String varName, VarValue result) {
		varTable.setVar(varName, result);
	}
	
	private void syntaxError() {
		System.out.println("Syntax Error at line: "+scanner.peekTokenLineNumber());
		System.out.println("Unrecognized token: "+scanner.peekToken());
		System.exit(0);
	}
	
	private void runtimeError(String token) {
		System.out.println("Runtime Error at line: "+scanner.peekTokenLineNumber());
		System.out.println("Variable not found: "+token);
		System.exit(0);
	}
	
	private void regexError(String regex) {
		System.out.println("Runtime Error at line: "+scanner.peekTokenLineNumber());
		System.out.println("Invalid regex: "+regex);
		System.exit(0);
	}

	public void executeMiniREFile() {
		System.out.println(executionOutput.toString());
	}
}
